/*
    libhdvbd - Library for building sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <glib.h>

#include <errno.h>
#include <string.h>

#include <unistd.h>

#include "codes.h"
#include "hdvb-stream-src.h"
#include "i18n.h"

#define HDVB_STREAM_SRC_TIMEOUT 5000

static gboolean
tsf_prepare(GSource *source, gint *timeout)
{
    *timeout = HDVB_STREAM_SRC_TIMEOUT;
    return FALSE;
}

static gboolean
tsf_timed_out(HdvbStreamSrc *ssrc)
{
    GTimeVal now;
    
    g_source_get_current_time(&ssrc->gsource, &now);
    return (now.tv_usec - ssrc->tock.tv_usec) / 1000 + 
            (now.tv_sec - ssrc->tock.tv_sec) * 1000 >=
            HDVB_STREAM_SRC_TIMEOUT;
}

static gboolean
tsf_check(GSource *source)
{
    HdvbStreamSrc *ssrc = (HdvbStreamSrc *) source;
    
    return ssrc->poll.revents != 0 || tsf_timed_out(ssrc);
}

static int
find_sync_byte(HdvbStreamSrc *ssrc, int start, int filled)
{
    int n;
    
    for (n = start; n < filled - 188; ++n)
    {
        if (ssrc->buffer[n] == HDVB_TS_PACKET_SYNC_BYTE &&
                ssrc->buffer[n + 188] == HDVB_TS_PACKET_SYNC_BYTE)
        {
            return n;
        }
    }
    return -1;
}

static gboolean
tsf_dispatch(GSource *source, GSourceFunc callback, gpointer handle)
{
    HdvbStreamSrc *ssrc = (HdvbStreamSrc *) source;
    int filled;
    size_t nread = read(ssrc->poll.fd, ssrc->buffer + ssrc->remainder,
            HDVB_STREAM_SRC_BUF_SIZE - ssrc->remainder);
    int offset;
    
    if (nread <= 0)
    {
        GList *link;
        
        g_warning(_("Error reading stream from receiver %d:%d: %s"),
                ssrc->rcv->adapter, ssrc->rcv->frontend, strerror(errno));
        g_static_rec_mutex_lock(&ssrc->lock);
        for (link = ssrc->taps; link; link = g_list_next(link))
        {
            HdvbStreamTap *tap = HDVB_STREAM_TAP(link->data);
            
            hdvb_stream_tap_src_closed(tap);
        }
        ssrc->remainder = 0;
        g_static_rec_mutex_unlock(&ssrc->lock);
        return TRUE;
    }
    
    g_source_get_current_time(&ssrc->gsource, &ssrc->tock);
    filled = ssrc->remainder + nread;
    if (filled < 188 * 2 - 1)
    {
        /* Not enough data to detect a packet */
        ssrc->remainder = filled;
        return TRUE;
    }
    
    if (ssrc->synced)
    {
        offset = 0;
    }
    else
    {
        offset = find_sync_byte(ssrc, 0, filled);
        if (offset == -1)
        {
            g_warning(_("Received TS data with no sync bytes"));
            ssrc->remainder = 0;
            return TRUE;
        }
        ssrc->synced = TRUE;
    }
    
    g_static_rec_mutex_lock(&ssrc->lock);
    while (offset < filled - 187)
    {
        int pid;
        GPtrArray *taps;
        
        if (ssrc->buffer[offset] != HDVB_TS_PACKET_SYNC_BYTE)
        {
            g_warning(_("TS data missing expected sync byte"));
            offset = find_sync_byte(ssrc, offset, filled);
            if (offset == -1)
            {
                ssrc->synced = FALSE;
                ssrc->remainder = 0;
                g_static_rec_mutex_unlock(&ssrc->lock);
                return TRUE;
            }
        }
        
        pid = ((ssrc->buffer[offset] << 8) & 0x1f) | ssrc->buffer[offset + 1];
        taps = g_hash_table_lookup(ssrc->pids, GINT_TO_POINTER(pid));
        if (taps)
        {
            int n;
            
            for (n = 0; n < taps->len; ++n)
            {
                hdvb_stream_tap_add_packet(
                        HDVB_STREAM_TAP(g_ptr_array_index(taps, n)),
                        ssrc->buffer + offset);
            }
        }
        offset += 188;
    }
    if (offset < filled)
    {
        memmove(ssrc->buffer, ssrc->buffer + offset, filled - offset);
        ssrc->remainder = filled - offset;
    }
    else
    {
        ssrc->remainder = 0;
    }
    g_static_rec_mutex_unlock(&ssrc->lock);
    return TRUE;
}

static void
hdvb_stream_src_rcv_detuned(HdvbStreamSrc *ssrc,
        const char *new_tuning, HdvbReceiver *rcv)
{
    hdvb_stream_src_delete(ssrc);
}

HdvbStreamSrc *
hdvb_stream_src_new(HdvbReceiver *rcv)
{
    static GSourceFuncs source_funcs = {
            tsf_prepare, tsf_check, tsf_dispatch,
            NULL, NULL, NULL };
    HdvbStreamSrc *ssrc = (HdvbStreamSrc *)
            g_source_new(&source_funcs, sizeof(HdvbStreamSrc));
    
    g_static_rec_mutex_init(&ssrc->lock);
    ssrc->rcv = rcv;
    ssrc->poll.fd = -1;
    ssrc->poll.events = G_IO_IN | G_IO_HUP | G_IO_ERR;
    ssrc->rcv = NULL;
    ssrc->pids = NULL;
    ssrc->detune_tag = g_signal_connect_swapped(rcv, "detune",
            G_CALLBACK(hdvb_stream_src_rcv_detuned), ssrc);
    ssrc->deleting = FALSE;
    ssrc->remainder = 0;
    ssrc->synced = FALSE;
    return ssrc;
}

static void
hdvb_stream_src_delete_full(HdvbStreamSrc *ssrc, gboolean already_locked)
{
    GList *link, *next;
    
    if (!already_locked)
        g_static_rec_mutex_lock(&ssrc->lock);
    /* Deleting all taps will cause this function to be reentered */
    if (ssrc->deleting)
    {
        if (!already_locked)
            g_static_rec_mutex_unlock(&ssrc->lock);
        return;
    }
    ssrc->deleting = TRUE;
    
    if (ssrc->poll.fd != -1)
    {
        g_source_remove_poll(&ssrc->gsource, &ssrc->poll);
        g_source_destroy(&ssrc->gsource);
        hdvb_receiver_close_dvr(ssrc->rcv);
        ssrc->poll.fd = -1;
    }
    
    g_signal_handler_disconnect(ssrc->rcv, ssrc->detune_tag);
    hdvb_receiver_remove_stream_src(ssrc->rcv);
    for (link = ssrc->taps; link; link = next)
    {
        next = g_list_next(link);
        hdvb_stream_tap_src_closed(HDVB_STREAM_TAP(link->data));
    }
    /* Tap implementations should have deleted themselves by now */
    g_static_rec_mutex_unlock(&ssrc->lock);
    g_static_rec_mutex_free(&ssrc->lock);
    g_free(ssrc);
}

void
hdvb_stream_src_delete(HdvbStreamSrc *ssrc)
{
    hdvb_stream_src_delete_full(ssrc, FALSE);
}

static gboolean
hdvb_stream_src_request_pid(HdvbStreamSrc *ssrc, HdvbStreamTap *tap,
        int pid, dmx_pes_type_t pes_type)
{
    GPtrArray *taps_for_pid;
    
    if (hdvb_receiver_request_ts_filter(ssrc->rcv, pid, pes_type))
    {
        g_warning(_("Failed to request TS filter for pid %d"), pid);
        return FALSE;
    }
    taps_for_pid = g_hash_table_lookup(ssrc->pids, GINT_TO_POINTER(pid));
    if (!taps_for_pid)
    {
        taps_for_pid = g_ptr_array_new();
        g_hash_table_insert(ssrc->pids, GINT_TO_POINTER(pid), taps_for_pid);
    }
    g_ptr_array_add(taps_for_pid, tap);
    return TRUE;
}

static void
hdvb_stream_src_cancel_pid(HdvbStreamSrc *ssrc, HdvbStreamTap *tap, int pid)
{
    GPtrArray *taps_for_pid;
    
    hdvb_receiver_unref_ts_filter(ssrc->rcv, pid);
    taps_for_pid = g_hash_table_lookup(ssrc->pids, GINT_TO_POINTER(pid));
    if (taps_for_pid)
    {
        g_ptr_array_remove(taps_for_pid, tap);
        if (!taps_for_pid->len)
        {
            g_hash_table_remove(ssrc->pids, GINT_TO_POINTER(pid));
            g_ptr_array_free(taps_for_pid, TRUE);
        }
    }
}

gboolean
hdvb_stream_src_add_tap(HdvbStreamSrc *ssrc, HdvbStreamTap *tap)
{
    const HdvbServicePidsInfo *pids_info = hdvb_stream_tap_get_pids_info(tap);
    GList *link;
    gboolean pcr_ok = FALSE;
    gboolean pat_ok = FALSE;
    gboolean pmt_ok = FALSE;
    gboolean all_ok = FALSE;
    
    g_static_rec_mutex_lock(&ssrc->lock);
    if (!ssrc->pids)
        ssrc->pids = g_hash_table_new(g_direct_hash, g_direct_equal);
    if (hdvb_stream_src_request_pid(ssrc, tap,
            pids_info->pcr_pid, DMX_PES_OTHER))
    {
        pcr_ok = TRUE;
    }
    if (pcr_ok && hdvb_stream_src_request_pid(ssrc, tap,
            HDVB_PID_PAT, DMX_PES_OTHER))
    {
        pat_ok = TRUE;
    }
    if (pat_ok && !hdvb_stream_src_request_pid(ssrc, tap,
            pids_info->pmt_pid, DMX_PES_OTHER))
    {
        pmt_ok = TRUE;
    }
    if (pmt_ok)
    {
        all_ok = TRUE;
        for (link = pids_info->streams; link; link = g_list_next(link))
        {
            HdvbStreamInfo *si = link->data;
            
            if (!hdvb_stream_src_request_pid(ssrc, tap, si->pid, si->pes_type))
            {
                /* Cancel all pids added for this tap so far */
                while ((link = g_list_previous(link)) != NULL)
                {
                    si = link->data;
                    hdvb_stream_src_cancel_pid(ssrc, tap, si->pid);
                }
                all_ok = FALSE;
                break;
            }
        }
        if (all_ok)
        {
            ssrc->taps = g_list_append(ssrc->taps, tap);
            if (ssrc->poll.fd == -1)
            {
                g_get_current_time(&ssrc->tock);
                ssrc->poll.fd = hdvb_receiver_open_dvr(ssrc->rcv);
                g_source_add_poll(&ssrc->gsource, &ssrc->poll);
                g_source_attach(&ssrc->gsource, g_main_context_default());
            }
        }
    }
    if (!all_ok)
    {
        if (pmt_ok)
            hdvb_stream_src_cancel_pid(ssrc, tap, pids_info->pmt_pid);
        if (pat_ok)
            hdvb_stream_src_cancel_pid(ssrc, tap, HDVB_PID_PAT);
        if (pcr_ok)
            hdvb_stream_src_cancel_pid(ssrc, tap, pids_info->pcr_pid);
    }
    g_static_rec_mutex_unlock(&ssrc->lock);
    return all_ok;
}

void
hdvb_stream_src_remove_tap(HdvbStreamSrc *ssrc, HdvbStreamTap *tap)
{
    const HdvbServicePidsInfo *pids_info = hdvb_stream_tap_get_pids_info(tap);
    GList *link;
    
    g_static_rec_mutex_lock(&ssrc->lock);
    hdvb_stream_src_cancel_pid(ssrc, tap, pids_info->pcr_pid);
    hdvb_stream_src_cancel_pid(ssrc, tap, HDVB_PID_PAT);
    hdvb_stream_src_cancel_pid(ssrc, tap, pids_info->pmt_pid);
    for (link = pids_info->streams; link; link = g_list_next(link))
    {
        HdvbStreamInfo *si = link->data;
        
        hdvb_stream_src_cancel_pid(ssrc, tap, si->pid);
    }
    ssrc->taps = g_list_remove(ssrc->taps, tap);
    if (!ssrc->taps)
        hdvb_stream_src_delete_full(ssrc, TRUE);
    else
        g_static_rec_mutex_unlock(&ssrc->lock);
}
